iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
佛心分享-IT 人職涯歷練

從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始系列 第 17 篇

Day 17|push() 明明有加成功,為什麼 React 還是不建議直接改 State?

  • 分享至 

  • xImage
  •  

上一篇講到:

[...items]

以及:

{ ...data }

它們常常被用來建立新的 Array 或 Object。

到了 React 裡,這種寫法更常出現:

setMemos(prev => [
  ...prev,
  newMemo,
]);

第一次看到時,我其實很容易想:

為什麼要這麼麻煩?

因為 JavaScript 明明有:

push()

直接:

memos.push(newMemo);

不是更簡單嗎?

而且 push() 也真的有成功把資料加進去。

問題就在這裡:

「資料有被改到」和「React 正確知道資料更新了」是兩件事。


先看 JavaScript 裡的 push()

假設:

const memos = ["買牛奶"];

執行:

memos.push("寫文章");

現在:

memos

確實變成:

["買牛奶", "寫文章"]

所以:

push() 本身完全沒有錯。

它的工作就是:

直接修改原本的 Array。

這種「直接修改原資料」的行為,通常稱為:

Mutation

Mutation 可以先理解成「改原本那一份」

例如:

const user = {
  name: "小潔",
};

user.name = "狗勾";

原本的:

user

直接被修改了。

Array 也是:

const items = [1, 2];

items.push(3);

原本:

items

被改成:

[1, 2, 3]

所以:

push()
直接改 Object property
splice()
sort()

這類操作都有可能直接修改原資料。


React State 為什麼不喜歡這樣?

假設:

const [memos, setMemos] =
  useState(["買牛奶"]);

如果直接:

memos.push("寫文章");

JavaScript 裡的 memos 確實被改了。

但是我們沒有呼叫:

setMemos()

React 不會因為我們偷偷改了 Array 裡面的內容,就自動知道:

「欸,State 更新了,我現在要重新 Render。」

所以可能出現:

資料其實改了
↓
React 沒有正常收到 State 更新通知
↓
畫面沒有照預期更新

這就是第一個問題。


那我 push() 完再 setMemos(memos) 不就好了?

例如:

memos.push("寫文章");

setMemos(memos);

看起來合理很多。

但還有一個問題:

memos

前後其實還是同一個 Array。

可以把它想成:

修改前

memos ─────→ Array A

push()

memos ─────→ Array A
              ↑
          還是同一份

內容變了,

但 Array 本身的 reference 沒有變。


Reference 可以先想成「這是不是同一份東西」

例如:

const a = [1, 2];
const b = a;

這時:

a === b

結果是:

true

因為:

a
b

其實都指向同一個 Array。

圖像化:

a ───┐
     ↓
   [1, 2]
     ↑
b ───┘

如果:

b.push(3);

那:

a

也會變成:

[1, 2, 3]

因為根本就是同一份資料。


Spread 會建立新的 Array

如果改成:

const b = [...a];

現在:

a === b

結果是:

false

因為:

a ─────→ Array A
         [1, 2]

b ─────→ Array B
         [1, 2]

內容一樣,

但它們是兩個不同的 Array。

所以 React State 更新常看到:

setMemos(prev => [
  ...prev,
  "寫文章",
]);

流程:

prev
↓
舊 Array

...prev
↓
把舊內容展開

加入新資料
↓
建立新的 Array

setMemos()
↓
React 收到新的 State

這就是 Immutability 的基本概念

React 裡很常聽到:

Immutability

先不用把它想得很學術。

目前可以理解成:

不要直接修改原本的 State,而是根據舊資料建立一份新的資料。

例如:

不建議

user.name = "New Name";
setUser(user);

比較常見

setUser(prev => ({
  ...prev,
  name: "New Name",
}));

原本:

{
  name: "小潔",
  age: 31,
}

變成一個新的 Object:

{
  name: "New Name",
  age: 31,
}

為什麼 React 喜歡新的 Reference?

React 很依賴資料前後的變化來判斷:

這份資料是不是更新了?

如果:

舊 State
↓
Array A

新 State
↓
還是 Array A

變化會比較難追蹤。

但:

舊 State
↓
Array A

新 State
↓
Array B

就很清楚:

這是一份新的資料。

所以:

[...prev]

或:

{ ...prev }

不只是「寫法比較潮」。

它其實是在幫我們建立新的 reference。


Array 常見的更新方式

假設:

const items = [
  { id: 1, name: "A" },
  { id: 2, name: "B" },
];

新增

不要直接:

items.push(newItem);

可以:

const next = [
  ...items,
  newItem,
];

刪除

不要直接:

items.splice(index, 1);

可以用:

const next = items.filter(
  item => item.id !== targetId
);

因為:

filter()

會回傳新的 Array。


修改其中一筆

可以:

const next = items.map(item =>
  item.id === targetId
    ? {
        ...item,
        name: "New Name",
      }
    : item
);

流程:

每一筆 item
↓
是我要改的嗎?

不是
→ 原樣回傳

是
→ 建立新的 Object
→ 修改 name

最後:

map()
↓
產生新的 Array

這也是 map() 為什麼在 React 很常見

前幾篇講 map() 時,我們說:

把 Array 轉換成新的 Array。

到了 State 更新裡,它又剛好非常適合:

舊 Array
↓
map()
↓
新的 Array

例如:

setItems(prev =>
  prev.map(item =>
    item.id === targetId
      ? {
          ...item,
          name: "Updated",
        }
      : item
  )
);

可以拆成:

拿舊 items
↓
逐筆檢查
↓
找到 target
↓
建立新的 item
↓
其餘維持原資料
↓
組成新的 Array
↓
setItems()

這些前面學過的東西其實都開始接起來了:

map()
+
Spread
+
State
+
Immutability

巢狀 Object 又更容易踩坑

例如:

const user = {
  name: "小潔",
  company: {
    name: "ABC",
  },
};

如果:

const next = {
  ...user,
};

我們昨天有提過:

這只是 Shallow Copy。

所以:

next.company.name = "XYZ";

仍然可能改到原本的:

user.company.name

因為:

user.company

跟:

next.company

還是同一份 Object。


所以巢狀 State 要一層一層建立新的資料

例如:

setUser(prev => ({
  ...prev,
  company: {
    ...prev.company,
    name: "XYZ",
  },
}));

流程:

舊 user
↓
建立新的 user

舊 company
↓
建立新的 company

name
↓
改成 XYZ

最後:

新的 user
└─ 新的 company

而不是偷偷修改原來的巢狀 Object。


為什麼這對 Debug 也很重要?

如果資料可以到處直接被修改:

data.name = ...
data.company.name = ...
items.push(...)
items.splice(...)

當某個值突然變掉時,很難知道:

到底是哪一段程式改的?

但如果都透過:

建立新資料
↓
setState

資料變化的入口會比較清楚。

例如:

setItems(...)

就可以直接找到:

這裡發生了一次 State 更新。

所以 Immutability 不只是 React Render 的問題。

它也讓:

資料流
State 變更
Debug

比較容易追。


但不是所有 JavaScript 都禁止 Mutation

這裡也要分清楚。

不是說:

push()
splice()
sort()

都是壞東西。

例如:

const temp = [];
temp.push("A");
temp.push("B");

如果它只是一個區域變數,而且沒有 React State 的問題,完全可以使用。

重點不是:

push() 永遠不能用。

而是:

不要直接拿 Mutation 的方式修改 React State。


我現在看到 State 更新,會先問

例如:

setItems(...)

我會看:

新的值是不是新 Array / Object?

還是程式其實先修改了原本 State?

例如:

看到這種

items.push(newItem);
setItems(items);

我會警覺:

原本 items 被 mutate 了

看到這種

setItems(prev => [
  ...prev,
  newItem,
]);

可以理解成:

根據舊資料
↓
建立新 Array
↓
交給 React

今天把前幾天的東西串起來

到這裡其實已經可以看到:

Day 15
map()
→ Array 轉成新的 Array

Day 16
Spread
→ 根據舊資料建立新 Array / Object

Day 17
Immutability
→ 不直接修改 State
→ 建立新資料再更新

所以這些語法不是三個獨立的知識點。

它們在 React 裡其實常常一起出現:

setItems(prev =>
  prev.map(item =>
    item.id === targetId
      ? {
          ...item,
          name: "Updated",
        }
      : item
  )
);

現在再看這段,可以拆成:

prev
→ 舊 State

map()
→ 建立新 Array

item.id === targetId
→ 找到要改的資料

...item
→ 保留原欄位

name: "Updated"
→ 覆蓋其中一個欄位

setItems()
→ 更新 State

原本像咒語的一整坨,其實每一塊都有自己的工作。


今天真正想記的是

Mutation
→ 直接改原本資料

Immutability
→ 不直接改原本資料
→ 建立新的 Array / Object

在 React State 裡,通常會偏向後者。

所以:

push()

不是因為「不能新增資料」。

而是它會:

直接修改原本 Array。

React 更常希望我們:

舊 State
↓
建立新的資料
↓
setState()
↓
React Render

當我理解這件事後,再看到滿畫面的:

...
map()
filter()

也比較不會覺得工程師只是故意把程式寫複雜。

很多時候,它們其實都是在做同一件事:

讓資料的變化保持清楚、可追蹤。

下一篇可以接一個在 React List 裡同樣很常見的問題:

key 到底是幹嘛的?為什麼不能每次都直接用 index?


上一篇
Day 16|... 到底是在展開什麼?Spread Operator 一次看懂
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言